(function () {
    if (window.DrawingLib) return;
    const GAP = 10, GAP2 = 2 * GAP;

    window.DrawingLib = function () {
        const DefaultLength = 80, PI2 = Math.PI * 2, K = 0.44, G = DefaultLength * DefaultLength,
            DefaultWidth = 800, DefaultHeight = 600;
        const links = {}, table = {}, hovered_stroke = '5px #ccc', HANDLERS = {
            dragAndDrop : {
                move: function () {
                    if (this.userData) {
                        this.userData.move ();
                    }
                }
            },
            on_node_over : function () {
                this.backupStroke = this.stroke;
                this.stroke = hovered_stroke;
                canvas.mouse.cursor('pointer');
                this.redraw ();
            },
            on_node_out : function () {
                this.stroke = this.backupStroke;
                canvas.mouse.cursor ('default');
                delete this.backupStroke;
                this.redraw ();
            }
        }, STYLES = {
            text_base : {
                origin: { x: "left", y: "top" },
                font: "14px sans-serif",
                fill: '#fff'
            },
            title: {
                origin: { x: "center", y: "top" },
                font: "bold 18px sans-serif",
                fill: '#fff'
            },
            host_base : {
                origin: { x: "center", y: "top" },
                font: "12px sans-serif",
                fill: '#ccc'
            },
            box_base : {fill:'#0aa',zIndex: 100}
        }, init_rect = function (rect, text, node) {
            rect.width = text.width + GAP2;
            rect.height = text.height + GAP2;
            rect.userData = node;
            text.x = text.y = GAP;
            rect.bind ('mouseenter', HANDLERS.on_node_over).bind ('mouseleave', HANDLERS.on_node_out);
        }, distance = function (a, b) {
            let dx = b.x - a.x, dy = b.y - a.y;
            return Math.sqrt (dx * dx + dy * dy);
        }, tension = function (a, b) {
            let d = distance (a, b);
            if (d <= DefaultLength) {
                return Force.Zero;
            }
            let value = K * (d - DefaultLength), angle = Math.atan2 (b.y - a.y, b.x - a.x);
            return new Force (value, angle);
        }, repulsion = function (a, b) {
            let d = distance (a, b);
            if (d <= 0) return Force.Default;
            let value = G * a.m * b.m / (d * d), angle = Math.atan2 (a.y - b.y, a.x - b.x);
            return new Force (value, angle);
        };
        let canvas;

        // classes
        window.Link = function (a, b) {
            if (!canvas) {
                throw "DrawingLib not initialed";
            }
            this.id = Math.random ().toString ().substring (2);
            let line = canvas.display.line ({start:a.center, end:b.center, stroke: '1px #f0f'});
            a.addLink ('start', line);
            b.addLink ('end', line);

            this.a = a;
            this.b = b;
            this.line = line;
        };

        window.ServiceRouteFrame = function (opts) {
            this._ = oCanvas.extend ({namespace: '', host: '', port: 0, items: []}, opts || {});
        };
        window.ServiceRouteFrame.prototype = {
            get namespace () { return this._.namespace; },
            set namespace (value) { this._.namespace = value; },
            get host () { return this._.host; },
            set host (value) { this._.host = value; },
            get port () { return this._.port; },
            set port (value) { this._.port = value },
            get items () { return this._.items; }
        };

        window.ServiceRouteItem = function (opts) {
            this._ = oCanvas.extend ({endpoint:'', type:[], methods: []}, opts || {});
        };
        window.ServiceRouteItem.prototype = {
            get endpoint () { return this._.endpoint; },
            set endpoint (value) { this._.endpoint = value },
            get type () { return this._.type },
            get methods () { return this._.methods}
        };

        window.SRFNode = function () {
            const cache = {};
            let srf_node = function (srf, x, y) {
                let settings = {links: []}, name, id, temp;

                if (typeof srf === 'string') {
                    name = srf;
                } else {
                    if ('namespace' in srf) {
                        name = srf.namespace;
                    } else {
                        throw "name or namespace should be spec.";
                    }

                    if (('host' in srf) && ('port' in srf)) {
                        temp = 'from <<' + srf.host + ':' + srf.port + '>>';
                    }
                }

                this.id = id = Math.random().toString (16).substring (2);
                settings.name = name;

                x = x || 0;
                y = y || 0;

                let root  = canvas.display.rectangle (oCanvas.extend ({x:x,y:y}, STYLES.box_base)),
                    title = canvas.display.text (oCanvas.extend ({text: name}, STYLES.title)),
                    host, line, elements = [], i, e, n, w, h;

                init_rect (root, title, this);
                root.addChild (title, false);

                w = root.width;
                h = root.height;

                if (temp) {
                    host = canvas.display.text (oCanvas.extend ({text: temp}, STYLES.host_base));
                    root.addChild (host, false);
                    w = root.width = Math.max (w, host.width + GAP2);
                    host.y = h;
                    h = root.height = h + host.height + GAP;
                }

                if (srf.items && srf.items.length) {
                    line = canvas.display.line ({start: {x: 0, y: 0}, end: {x: 0, y: 0}, stroke: '1px #ff0'});
                    root.addChild (line, false);
                    line.start.y = line.end.y = h;

                    for (i = 0; i < srf.items.length; i ++) {
                        e = srf.items [i];
                        n = canvas.display.text (oCanvas.extend ({text: e.endpoint}, STYLES.text_base));
                        n.x = GAP;
                        n.y = h + GAP2;
                        h += n.height + GAP2;
                        w = Math.max (w, n.width + GAP2);
                        root.addChild (n, false);
                    }
                    h += GAP;
                    line.end.x = w;
                }
                title.x = w / 2;
                if (host) {
                    host.x = w / 2;
                }
                cache [id] = oCanvas.extend ({
                    srf: srf,
                    elements: elements,
                    root: root,
                    title: title,
                    width:w,
                    height:h,
                    x:x, y:y,
                    center: {x: x + w / 2, y: y + h / 2},
                    move: this.move
                }, settings);
                root.width = w;
                root.height = h;
                root.dragAndDrop (HANDLERS.dragAndDrop);
            };

            srf_node.prototype = {
                render : function () {
                    let root = cache [this.id].root;
                    if (root) {
                        canvas.addChild (root, false);
                    }
                },
                move : function () {
                    let _ = cache[this.id], e = _.root, x, y, w = _.width, h = _.height, c = _.center, i, link;
                    x = _.x = e.x;
                    y = _.y = e.y;
                    c.x = x + w / 2;
                    c.y = y + h / 2;

                    for (i = 0; i < _.links.length; i ++) {
                        link = _.links[i];

                        if (link.endpoint === 'start') {
                            link.line.start.x = c.x;
                            link.line.start.y = c.y;
                        } else {
                            link.line.end.x = c.x;
                            link.line.end.y = c.y;
                        }
                    }
                },
                addLink : function (endpoint, link) {
                    cache[this.id].links.push ({endpoint: endpoint, line: link});
                },

                get center () { return cache[this.id].center; },
                get width () { return cache[this.id].width; },
                get height () { return cache[this.id].height; },
                get x () {
                    let ctx = cache[this.id];
                    return ctx.x;
                },
                set x (x) {
                    let ctx = cache[this.id];
                    ctx.x = ctx.root.x = x;
                },
                get y () {
                    let ctx = cache[this.id];
                    return ctx.y;
                },
                set y (y) {
                    let ctx = cache[this.id];
                    ctx.y = ctx.root.y = y;
                },
/*
                get x () { return this.center.x; },
                set x (x) {
                    let ctx = cache [this.id], w = this.width;
                    ctx.root.x = x - w / 2;
                },
                get y () { return this.center.y; },
                set y (y) {
                    let ctx = cache [this.id], h = this.height;
                    ctx.root.y = y - h / 2;
                },
*/
                get f () {
                    let ctx = cache [this.id];
                    if (ctx.force) {
                        return ctx.force;
                    }

                    return ctx.force = new Force (10, Math.random () * Math.PI * 2);
                },
                get m () {
                    let ctx = cache [this.id];
                    return ctx.links.length;
                }
            };

            return srf_node;
        }();

        let Force = function (value, vector) {
            this.angle = vector;
            this.value = value;
            this.x = Math.cos (vector) * value;
            this.y = Math.sin (vector) * value;
        };
        Force.prototype = {
            add : function (force) {
                let dx = this.x + force.x, dy = this.y + force.y;
                this.value = Math.sqrt (dx * dx + dy * dy);
                this.angle = Math.atan2 (dy, dx);
                this.x = this.value * Math.cos (this.angle);
                this.y = this.value * Math.sin (this.angle);
            }
        };
        Force.Default = new Force (10, Math.random() * Math.PI * 2);
        Force.Zero = new Force (0, 0);

        // private functions
        (function() {
            let lastTime = 0;
            let vendors = ['webkit', 'moz'];
            for(let x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
                window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
                window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] ||    // Webkit中此取消方法的名字变了
                    window[vendors[x] + 'CancelRequestAnimationFrame'];

                window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] ||
                    window[vendors[x] + 'CancelAnimationFrame'] ||
                    window[vendors[x] + 'CancelRequestAnimationFrame'];
            }

            if (!window.requestAnimationFrame) {
                window.requestAnimationFrame = function(callback) {
                    let currTime = new Date().getTime();
                    let timeToCall = Math.max(0, 16.7 - (currTime - lastTime));
                    let id = window.setTimeout(function() {
                        callback(currTime + timeToCall);
                    }, timeToCall);
                    lastTime = currTime + timeToCall;
                    return id;
                };
            }
            if (!window.cancelAnimationFrame) {
                window.cancelAnimationFrame = function(id) {
                    clearTimeout(id);
                };
            }
        }());

        function addElement (element) {
            if (element instanceof Link) {
                links[element.id] = element;
            } else {
                table[element.id] = element;
            }
/*
            } else if (element instanceof SRFNode) {
                table[element.id] = element;
            } else {
                throw "only ElementNode and Link instance can be add."
            }
*/
        }

        function move (node) {
            if (node.locked) return;
            let d = node.f.value / node.m;
            node.x += d * Math.cos (node.f.angle);
            node.y += d * Math.sin (node.f.angle);

            node.f.angle = 0;
            node.f.value = 0;
            node.f.x = 0;
            node.f.y = 0;
        }

        function doLayout () {
            let k, link, nodes = [], i, j, a, b;
            for (k in links) if (links.hasOwnProperty (k)) {
                link = links [k];
                link.a.f.add (tension (link.a, link.b));
            }
            for (k in table) if (table.hasOwnProperty (k)) {
                nodes.push (table [k]);
            }
            for (i = 0; i < nodes.length; i ++) {
                for (j = 0; j < nodes.length; j ++) {
                    if (nodes [i] !== nodes [j]) {
                        nodes [i].f.add (repulsion (nodes[i], nodes[j]));
                    }
                }
            }
            for (i = 0; i < nodes.length;i ++) {
                move (nodes [i]);
            }
            requestAnimationFrame (function () {
                doLayout ();
            })
        }

        return {
            init: function (options) {
                if (!('canvas' in options)) {
                    throw 'canvas must be set!';
                }
                canvas = oCanvas.create ({canvas: options ['canvas']});

                return this;
            },
            render: function () {
                let k;
                for (k in links) if (links.hasOwnProperty (k)) {
                    canvas.addChild (links [k].line, false);
                }
                for (k in table) if (table.hasOwnProperty (k)) {
                    table[k].render ();
                }

                canvas.redraw ();
            },
            reflesh : function () {
                canvas.redraw ();
            },
            add : function () {
                for (let i = 0; i < arguments.length; i ++) {
                    let element = arguments [i];
                    if (Array.isArray (element)) {
                        for (let j = 0; j < element.length; j ++) {
                            addElement (element [j]);
                        }
                    } else {
                        addElement (element);
                    }
                }

                return this;
            },
            doLayout : function () {
                doLayout ();
            }
        };
    } ();
})();